use std::{
    net::SocketAddr,
    process::{Command, Stdio},
    sync::Arc,
    time::{Duration, Instant},
};

use axum::{
    routing::{get, post},
    Router,
};
use clap::Parser;
use serde_json::{json, Value};
use tokio::net::TcpListener;
use tokio::sync::{broadcast, RwLock};
use tokio::time::sleep;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

mod api;
mod cli;
mod config;
mod monitor;

use api::{
    http::{
        admin_reload_handler, admin_status_handler, admin_stop_handler, dns_handler,
        get_network_stats, get_system_stats, http_bench_handler, ping_handler, portscan_handler,
        tcping_handler, ws_bench_handler,
    },
    ws::{ws_probe_upgrade, ws_upgrade},
    AppState, ServerCommand,
};
use cli::{handle_server_action, run_cli, Cli, ServerAction};
use config::Config;
use monitor::NicMonitor;

#[tokio::main]
async fn main() {
    tracing_subscriber::registry()
        .with(fmt::layer().with_target(false))
        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
        .init();

    let cli = Cli::parse();

    if matches!(&cli, Cli::RunServer) {
        if let Err(err) = run_server_process().await {
            print_error_json(err);
            std::process::exit(1);
        }
        return;
    }

    let raw_config = match load_config_arc() {
        Ok(cfg) => cfg,
        Err(err) => {
            print_error_json(err);
            std::process::exit(1);
        }
    };

    let monitor = NicMonitor::new();

    match cli {
        Cli::Start => match start_daemon(raw_config.clone()).await {
            Ok(value) => print_json(&value),
            Err(err) => {
                print_error_json(err);
                std::process::exit(1);
            }
        },
        Cli::Status => match handle_server_action(ServerAction::Status, raw_config.clone()).await {
            Ok((status, body)) => {
                let success = status.is_success();
                print_json(&body);
                if !success {
                    std::process::exit(1);
                }
            }
            Err(err) => {
                let payload = json!({
                    "code": 200,
                    "message": "success",
                    "data": {
                        "running": false,
                        "reason": err
                    }
                });
                print_json(&payload);
            }
        },
        Cli::Reload => match handle_server_action(ServerAction::Reload, raw_config.clone()).await {
            Ok((status, body)) => {
                let success = status.is_success();
                print_json(&body);
                if !success {
                    std::process::exit(1);
                }
            }
            Err(err) => {
                print_error_json(err);
                std::process::exit(1);
            }
        },
        Cli::Stop => match handle_server_action(ServerAction::Stop, raw_config.clone()).await {
            Ok((status, body)) => {
                let success = status.is_success();
                print_json(&body);
                if !success {
                    std::process::exit(1);
                }
            }
            Err(err) => {
                print_error_json(err);
                std::process::exit(1);
            }
        },
        Cli::RunServer => unreachable!("internal run-server command handled earlier"),
        command => {
            run_cli(command, &monitor).await;
        }
    }
}

async fn run_server(
    config: Arc<RwLock<Config>>,
    monitor: NicMonitor,
    shutdown_tx: broadcast::Sender<ServerCommand>,
) -> Result<(), String> {
    let current_config = config.read().await.clone();
    let addr = SocketAddr::from(([0, 0, 0, 0], current_config.server.http_port));
    drop(current_config);

    let listener = TcpListener::bind(addr)
        .await
        .map_err(|err| format!("Failed to bind TCP listener: {err}"))?;

    tracing::info!("Server running on http://{addr}");

    let state = AppState::new(config.clone(), monitor, shutdown_tx.clone(), Instant::now());

    let app = Router::new()
        .route("/api/system", get(get_system_stats))
        .route("/api/network", get(get_network_stats))
        .route("/api/ping", get(ping_handler))
        .route("/api/tcping", get(tcping_handler))
        .route("/api/dns", get(dns_handler))
        .route("/api/portscan", get(portscan_handler))
        .route("/api/http_bench", get(http_bench_handler))
        .route("/api/ws_bench", get(ws_bench_handler))
        .route("/api/admin/status", get(admin_status_handler))
        .route("/api/admin/reload", post(admin_reload_handler))
        .route("/api/admin/stop", post(admin_stop_handler))
        .route("/ws", get(ws_upgrade))
        .route("/ws/probe", get(ws_probe_upgrade))
        .with_state(state);

    let std_listener = listener
        .into_std()
        .map_err(|err| format!("Failed to convert tokio listener into std listener: {err}"))?;

    let mut shutdown_rx = shutdown_tx.subscribe();

    axum::Server::from_tcp(std_listener)
        .map_err(|err| format!("Failed to create Axum server from listener: {err}"))?
        .serve(app.into_make_service())
        .with_graceful_shutdown(async move {
            loop {
                match shutdown_rx.recv().await {
                    Ok(ServerCommand::Stop) => break,
                    Err(broadcast::error::RecvError::Closed) => break,
                    Err(broadcast::error::RecvError::Lagged(_)) => continue,
                }
            }
            tracing::info!("Shutdown signal received, stopping server");
        })
        .await
        .map_err(|err| format!("Server terminated unexpectedly: {err}"))?;

    Ok(())
}

async fn run_server_process() -> Result<(), String> {
    let config = load_config_arc()?;
    let monitor = NicMonitor::new();
    let (shutdown_tx, _) = broadcast::channel(16);
    run_server(config, monitor, shutdown_tx).await
}

fn load_config_arc() -> Result<Arc<RwLock<Config>>, String> {
    Config::load().map(|cfg| Arc::new(RwLock::new(cfg)))
}

async fn start_daemon(config: Arc<RwLock<Config>>) -> Result<Value, String> {
    if let Ok((status, body)) = handle_server_action(ServerAction::Status, config.clone()).await {
        if status.is_success()
            && body
                .get("data")
                .and_then(|data| data.get("running"))
                .and_then(|running| running.as_bool())
                .unwrap_or(false)
        {
            return Ok(json!({
                "code": 409,
                "message": "server already running",
                "data": body
            }));
        }
    }

    let exe_path =
        std::env::current_exe().map_err(|err| format!("failed to locate executable: {err}"))?;

    let mut command = Command::new(exe_path);
    command.arg("__run_server");
    command.stdin(Stdio::null());
    command.stdout(Stdio::null());
    command.stderr(Stdio::null());

    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        const DETACHED_PROCESS: u32 = 0x00000008;
        command.creation_flags(DETACHED_PROCESS);
    }

    let mut child = command
        .spawn()
        .map_err(|err| format!("failed to spawn server process: {err}"))?;
    let pid = child.id();

    for _ in 0..20 {
        if let Ok((status, body)) = handle_server_action(ServerAction::Status, config.clone()).await
        {
            if status.is_success() {
                return Ok(json!({
                    "code": 200,
                    "message": "server started",
                    "data": {
                        "pid": pid,
                        "status": body
                    }
                }));
            }
        }
        sleep(Duration::from_millis(250)).await;
    }

    let _ = child.kill();
    let _ = child.wait();

    Err("server failed to start within timeout".to_string())
}

fn print_json(value: &Value) {
    println!("{}", serde_json::to_string(value).unwrap());
}

fn print_error_json<S: AsRef<str>>(message: S) {
    let payload = json!({
        "code": 500,
        "message": message.as_ref(),
        "data": null
    });
    print_json(&payload);
}
